/**
 * @file       godemool/godemool.cc
 * @brief      Main.
 * @author     ben "boin" bonoeil
 * @date       2015-02
 */

/* Copyright (c) 2007-2015, ben "boin" bonoeil

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions
   are met:
   * Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
   copyright notice, this list of conditions and the following
   disclaimer in the documentation and/or other materials provided
   with the distribution.
   * Neither the name of the ben "boin" bonoeil nor the names of its
   contributors may be used to endorse or promote products derived
   from this software without specific prior written permission.

   NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE
   GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY
   ben "boin" bonoeil ''AS IS'' AND ANY EXPRESS OR IMPLIED
   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   DISCLAIMED. IN NO EVENT SHALL ben "boin" bonoeil BE LIABLE FOR ANY
   DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
   GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
   WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#ifdef _MSC_VER
# define _CRT_SECURE_NO_WARNINGS 1
# define strcasecmp(A,B) _stricmp((A),(B))
#endif

#include "godemool/error.h"
#include "godemool/print.h"
#include "godemool/decoder.h"

#include "godemool/nulprinter.h"
#include "godemool/xmlprinter.h"

#include <typeinfo>
#include <cassert>
#include <cstring>
#include <vector>
#include <iostream>
#include <fstream>
#include <sstream>

using namespace boin;

static const char me[] = PACKAGE_NAME;

enum {
  output_XML, output_DML, output_TXT, output_JSON, output_MAX
};
static const char * fmt_names[] = {
  "xml","dml","txt","json"
};
static const int fmt_max = sizeof(fmt_names)/sizeof(*fmt_names);

static int opt_verbose = 2;
static int opt_debug   = 0;
static int opt_deep    = 0;
static int opt_format  = output_TXT;
static int opt_header  = 0;

namespace boin {
  struct Parms {
    struct File {
      const char * name;
      std::fstream * file;

      File(): name(""), file(nullptr) {}
      ~File() { Close(); }
      bool IsOpen() const { return file && file->is_open(); }
      void Close() {
        if (file) {
          if (file->is_open()) file->close();
          delete file;
        }
      }

    } inp, out;
  };
}

typedef struct boin::Parms parms_t;

typedef int (*cmd_f)(parms_t *, Hl2Demo *);

static int dump_fct(parms_t *, Hl2Demo *);
static int copy_fct(parms_t *, Hl2Demo *);
static int info_fct(parms_t *, Hl2Demo *);
static int test_fct(parms_t *, Hl2Demo *);

static const char * arg1[] = {"<in>",0};
static const char * arg2[] = {"<in>","<out>",0};

static struct cmd_s {
  const char  * name;
  const char ** args;
  cmd_f         func;
  const char  * desc;

} cmds[] = {
  { "test", arg1, test_fct, "test for valid CS:GO demo file"    },
  { "info", arg1, info_fct, "display info "                     },
  { "copy", arg2, copy_fct, "copy file (with optionnal filter)" },
};
#define MAXMODES int(sizeof(cmds)/sizeof(*cmds))

/* Returns available format list (for usage). */
static const char * format_list(void)
{
  static char s[64];
  assert(fmt_max == output_MAX);
  for (int i=0, j=0; i<output_MAX; ++i) {
    if (j) s[j++] = '|';
    for (int k=0; !!(s[j] = fmt_names[i][k]) ; ++k, ++j);
  }
  return s;
}

/* Search format string. */
static int format_search(const char * what, int ifnotfound)
{
  assert(fmt_max == output_MAX);
  if (what)
    for (int i=0; i<output_MAX; ++i)
      if (!strcasecmp(what, fmt_names[i]))
        return i;
  return ifnotfound;
}

static int print_usage(void)
{
  int i;
  static char tabs[] = "                 ";
  printf(
	 "Usage: %s [OPTION] action [<input-demo> [<output-demo>]]\n"
	 "\n"
	 "  An CS:GO demo file toolbox.\n"
	 "\n"
	 " Options:\n"
	 "  -h               print this help and exit\n"
	 "  -V               print version and copyright and exit\n"
	 "  -D               enable debug mode\n"
	 "  -v               increase verbosity\n"
	 "  -q               decrease verbosity\n"
	 "  -d               increase depth\n"
	 "  -H               header only\n"
	 "  -F <format>      set output format [%s]\n"
	 "\n"
	 " Actions:\n", PACKAGE_NAME, format_list());

  for (i=0 ; i != MAXMODES; ++i) {
    int j = 0, k;
    printf("  %s",cmds[i].name);
    for (k=0; cmds[i].args[k]; ++k) {
      printf(" %s", cmds[i].args[k]); j+= strlen(cmds[i].args[k])+1;
    }
    printf("%s%s\n",tabs+j,cmds[i].desc);
  }

  puts("\n"
       "Report bugs to " PACKAGE_BUGREPORT);

  return 0;
}

static int print_version(void)
{
  puts( PACKAGE_STRING
        "\n"
        "Copyright (c) 2007-2015, Ben \"Boin\" Bonoeil\n"
        "All rights reserved.");
  return 0;
}

#ifdef HAVE_GETOPT
# ifdef HAVE_UNISTD_H
#  include <unistd.h>
# else
extern "C" {
  int opterr, optind;
  int getopt(int argc, char * const argv[], const char *optstring);
}
# endif
#else
/* A very simplistic version of getopt() */
static int opterr, optind = 0, optsub;
static char * optarg = 0;
static int getopt(int argc, char * argv[], const char * options)
{
  int ret = -1; /* return code if no more arguments */
  if (!optind) {
    optind = 1; optsub = 0;
  }
  optarg = 0;
  while (optind < argc && argv[optind][0] == '-') {
    const char * pos;
    if (!argv[optind][1]) {
      /* "-" is a valid argument */
      break;
    } else if (argv[optind][1] == '-' && !argv[optind][2]) {
      /* "--" breaks parsing */
      ++optind; break;
    } else if (!argv[optind][++optsub]) {
      /* end of option string */
      ++optind; optsub = 0;
    } else if (pos = strchr(options, argv[optind][optsub]), pos) {
      /* known option */
      ret = argv[optind][optsub];
      if (pos[1] == ':') {
        /* option needs an argument */
        if (argv[optind][optsub+1]) {
          /* Unless it's empty, use the end of option string as argument */
          optarg = argv[optind]+optsub+1;
        } else if (optind+1 < argc) {
          /* Else get the next argument */
          optarg = argv[++optind];
        }
        ++optind; optsub = 0;
      }
      break;
    } else {
      /* unknown option */
      ret = '?';
      break;
    }
  }
  return ret;
}
#endif

#ifdef HAVE_BASENAME
# ifdef HAVE_LIBGEN_H
#  include <libgen.h>
# else
extern "C" char * basename(char * path);
# endif
#else
/* Simplistic version of basename */
static char * basename(char * path) {
  char * s1 = strrchr(path,'/');
#ifdef WIN32
  char * s2 = strrchr(path,'\\');
  if (!s2 && path[0] && path[1] == ':') s2 = path+1;
#else
  char * s2 = 0;
#endif
  if (s1 < s2) s1 = s2;
  return s1 ? s1+1 : path;
}
#endif

static std::string & fileaddress(std::string & errmsg, Parms::File & f)
{
  std::ostringstream res;

  res << '\'' << f.name << '\'';
  if (f.IsOpen()) {
    try {
      // HAX: don't know openmode nevermind test both.
      // It does not work as intended with gcc when the parser skip (seek)
      // data instead of read.
      std::streampos posg = f.file->tellg(), posp = f.file->tellp();
      res << ':' << std::max(posg,posp);
    } catch (...) {
    }
  }
  errmsg = res.str();
  return errmsg;
}


int main(int argc, char * argv[])
{
  int code = Error::error;
  parms_t p;
  const char * cmdstr = "";
  Hl2Demo * hl2demo = nullptr;

  memset(&p,0,sizeof(p));
  p.inp.name = p.out.name = "";
  msg_set_default();

  try {
    static const char * optstr="hVvqDF:dH";
    int opt;

    opterr = 0;                   /* disable getopt error print */
    while (opt = getopt(argc, argv, optstr), opt != -1) {
      switch (opt) {
      case 'h': print_usage(); return 0; break;
      case 'v': ++opt_verbose; break;
      case 'q': opt_verbose -= opt_verbose>0; break;
      case 'D': ++opt_debug; break;
      case 'V': print_version(); return 0;
      case 'd': ++opt_deep; break;
      case 'H': opt_header = 1;
      case 'F':
        opt_format = format_search(optarg, output_TXT); break;
      default:
        emsg("%s: invalid option -- '%c' (try -h)\n", me, opt);
        throw Error::args;
      }
    }

    imsg_set(opt_verbose >= 2 ? stdout : 0);
    dmsg_set(opt_debug        ? stdout : 0);
    emsg_set(opt_verbose      ? stderr : 0);
    wmsg_set(opt_verbose > 1  ? stderr : 0);

    if (optind >= argc) {
      emsg("%s: missing action (try -h)\n", me);
      throw Error::args;
    }

    int i, j;
    cmdstr = argv[optind++];
    for (i=0; i!=MAXMODES && strcmp(cmdstr, cmds[i].name); ++i)
      ;
    if (i == MAXMODES) {
      emsg("%s: invalid action -- '%s' (try -h)\n", me, cmdstr);
      throw Error::args;
    }

    /* Count needed arguments for this command. */
    for (j=0; cmds[i].args && cmds[i].args[j]; ++j)
      ;

    /* Check argument count. */
    //if (optind + j != argc) { // This is the right thing
    if (optind + j > argc) { // This is just for debugging
      emsg("%s: invalid argument count for action -- '%s' (try -h)\n",
           me, cmdstr);
      throw Error::args;
    }

    /* Setup input */
    if (optind < argc) {
      p.inp.name = argv[optind++];
      if (!p.inp.name[0]) {
        emsg("%s: invalid input filename\n", me);
        throw Error::args;
      }
    }

    /* Setup output */
    if (optind < argc) {
      p.out.name = argv[optind++];
      if (!p.out.name[0]) {
        emsg("%s: invalid output filename\n",me);
        throw Error::args;
      }
    }

    if (*p.inp.name) {
      p.inp.file = new std::fstream();
      if (!p.inp.file) {
        throw Error(Error::system);
      }
      p.inp.file->open(p.inp.name, std::fstream::binary|std::fstream::in);
      if (p.inp.file->fail()) {
        emsg("%s: failed to open -- '%s'\n", me, p.inp.name);
        throw Error::system;
      }
      const std::ios_base::iostate state = std::fstream::failbit
        | std::fstream::badbit | std::fstream::eofbit;
      p.inp.file->exceptions(state);
    }

    dmsg ("Output-mode : %s\n",fmt_names[opt_format]);
    dmsg ("Action      : %s\n",cmds[i].name);
    dmsg ("Input       : %s\n",p.inp.name);
    dmsg ("Output      : %s\n",p.out.name);

    /* If command has an input stream create the hl2demo object from it. This will load and check the demo header. */
    if (p.inp.file)
      hl2demo = new Hl2Demo(*p.inp.file);

    dmsg("after header position: %u\n", (unsigned int) p.inp.file->tellg());


    /* Run the command handler. */
    code = cmds[i].func(&p, hl2demo);

  } catch (Error::Num e) {
    code = e;
  } catch (Error error) {
    std::string errmsg;
    code = error;
    switch (error.num) {
    case Error::input:
      emsg("%s: %s -- %s\n", me, (const char *)error, fileaddress(errmsg, p.inp).c_str());
      break;
    case Error::output:
      emsg("%s: %s -- %s\n", me, (const char *)error, fileaddress(errmsg, p.out).c_str());
      break;
    default:
      emsg("%s: %s\n", me, (const char *)error);
      break;
    }
  } catch (...) {
    code = Error::error;
    emsg("%s: (%d) %s '%s' -- %s\n", me, code, cmdstr, p.inp.name, "unexpected error");
  }

  if (hl2demo)
    delete hl2demo;

  return code;
}

static int test_fct(parms_t * p, Hl2Demo * hl2demo)
{
  Decoder decoder;

  if (opt_verbose > 2)
    imsg("%sesting '%s'\n", opt_deep>0?"Deeply t":"T",p->inp.name);

  if (!opt_header) {
    Hl2Demo::Chunk * chk;
    int walkflags = (!opt_deep) ? Hl2Demo::WalkFlags::SKIPDATA : 0;
    do {
      chk = hl2demo->Walk(*p->inp.file, walkflags);
      assert(chk);
      assert(chk->IsValid());
      if (chk && chk->data.size)
        decoder.Decode(*chk);
    } while (!chk->IsStop());
  }
  if (opt_verbose > 1)
    imsg("Ok '%s'\n", p->inp.name);
  return 0;
}

static int info_fct(parms_t * p, Hl2Demo * hl2demo)
{
  //Decoder decoder;
  Hl2Demo::Chunk * chunk = nullptr;
  XmlPrinter printer(std::cout);

  //printer.SetDecoder(&decoder); // assign decoder (this is ugly temp)

  try
    {
      const Hl2Demo::WalkFlags flags(0);
      dmsg("info: '%s'\n", p->inp.name);
      IPrinter * prt = &printer;

      printer.Open(std::string(basename(const_cast<char *>(p->inp.name))),
		   hl2demo->header);
      do {
	chunk = hl2demo->Walk(*p->inp.file, flags);
	//imsg("#%04d pos:%8d tick:%7d (%d) data:%d\n",
	//     chunk->idx, chunk->pos, chunk->tick, chunk->type, chunk->data.size);
	CodecType * obj = printer.Decode(*chunk);
	if (obj)
	  prt->Print(chunk,obj);
      } while (chunk && !chunk->IsStop());

      printer.Close();

    } catch (BitStream::Flow & e) {
    dmsg("%s in chunk #%d (%d)\n", e.what(), chunk->idx, chunk->type);
    throw Error(Error::input,
                e < 0 ?  "broken (overflow)" : "broken (underflow)");
  }

  if (opt_verbose > 2)
    imsg("Ok '%s'\n", p->inp.name);
  return Error::ok;
}

static int copy_fct(parms_t * p, Hl2Demo * hl2demo)
{
  return Error::error;
}
